package com.example.palette.view;

import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Looper;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.DecelerateInterpolator;
import android.widget.OverScroller;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.text.BidiFormatter;
import androidx.core.view.ViewCompat;

import com.example.palette.R;
import com.example.palette.bean.Lrc;
import com.example.palette.util.LrcUtil;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class LrcView extends View {
    private static final String NO_LRC = "暂无歌词";
    private float mLrcTextSize;
    private float mLrcTextLineSpan;
    private float mIndicatorLineSpan;
    private float mIndicatorTextSize;
    private float mNolrcTextSize;
    private float mIndicatorIconLeftMargin;
    private float mIndicatorRightMargin;
    private float mIndicatorLineLeftToIconRightMargin;
    private float mIndicatorLineRightToTextLeftMargin;
    private float mIndicatorIconWidth;
    private float mIndicatorIconHeight;
    private float mActionDownX;
    private float mActionDownY;
    private float mScrollOffset;
    private int mTouchDelay;
    private int mIndicatorTouchDelay;
    private int mLrcCommonColor;
    private int mLrcPlayingColor;
    private int mNolrcTextColor;
    private int mIndicatorTextColor;
    private int mIndicatorLineColor;
    private int mIndicatorLrcLineColor;
    private int mScaledTouchSlop;
    private int mScaledMaximumFlingVelocity;
    private int mScaledMinimumFlingVelocity;
    private int mCurrentLine;
    private boolean mLrcCurrentLineTextBold;
    private boolean mLndicatorLrcLineTextBold;
    private boolean mIsActionDown;
    private boolean mIsActionMove;
    private boolean mShowIndicator;
    private boolean mAutoFixPosition = true;
    private String mNolrcText;
    private Drawable mIndicatorIcon;
    private OverScroller mOverScroller;
    private TextPaint mTextPaint;
    private Paint mIndicatorPaint;
    private Rect mIndicatorIconRect;
    private List<Lrc> mLrcs;
    private Map<String, StaticLayout> mLrcLayoutsMap = new HashMap<>();
    private Map<String, StaticLayout> mLrcHeightsMap = new HashMap<>();
    private VelocityTracker mVelocityTracker;
    private BidiFormatter.Builder mBFbuilder;
    private OnApplyIndicatorLineListener mOnApplyIndicatorLineListener;
    private Runnable mScrollRunnable = new Runnable() {
        @Override
        public void run() {
            mIsActionDown = false;
            scrollToPosition(mCurrentLine);
        }
    };
    private Runnable mHideIndicatorRunnable = new Runnable() {
        @Override
        public void run() {
            mShowIndicator = false;
            invalidateInner();
        }
    };

    public LrcView(Context context) {
        this(context, null);
    }

    public LrcView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LrcView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (changed) {
            mIndicatorIconRect.left = (int) mIndicatorIconLeftMargin;
            mIndicatorIconRect.top = (int) (getHeight() / 2 - mIndicatorIconHeight / 2);
            mIndicatorIconRect.right = (int) (mIndicatorIconRect.left + mIndicatorIconWidth);
            mIndicatorIconRect.bottom = (int) (mIndicatorIconRect.top + mIndicatorIconHeight);
            mIndicatorIcon.setBounds(mIndicatorIconRect);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (isLrcsEmpty()) {
            drawNolrcText(canvas);
            return;
        }
        int position = getIndicatorPosition();
        mTextPaint.setTextSize(mLrcTextSize);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
        float y = getHeight() / 2f;
        float x = getLrcWidth() / 2f + getPaddingLeft();
        for (int i = 0; i < mLrcs.size(); i++) {
            if (i > 0) {
                y += (getLineHeight(i - 1) + getLineHeight(i)) / 2f + mLrcTextLineSpan;
            }
            if (mCurrentLine == i) {
                mTextPaint.setColor(mLrcPlayingColor);
                mTextPaint.setFakeBoldText(mLrcCurrentLineTextBold);
            } else if (position == i && mShowIndicator) {
                mTextPaint.setColor(mIndicatorLrcLineColor);
                mTextPaint.setFakeBoldText(mLndicatorLrcLineTextBold);
            } else {
                mTextPaint.setFakeBoldText(false);
                mTextPaint.setColor(mLrcCommonColor);
            }
            drawLrc(canvas, x, y, i);
        }
        if (mShowIndicator) {
            mIndicatorIcon.draw(canvas);
            int time = mLrcs.get(position).getTime();
            String formatTime = LrcUtil.formatLrcTime(time);
            float timeWidth = mIndicatorPaint.measureText(formatTime);
            mIndicatorPaint.setColor(mIndicatorLineColor);
            canvas.drawLine(mIndicatorIconRect.right + mIndicatorLineLeftToIconRightMargin, getHeight() / 2f,
                    getWidth() - timeWidth - mIndicatorRightMargin - mIndicatorLineRightToTextLeftMargin, getHeight() / 2f, mIndicatorPaint);
            int bx = (int) (getWidth() - timeWidth - mIndicatorRightMargin);
            float bl = getHeight() / 2f - (mIndicatorPaint.descent() - mIndicatorPaint.ascent()) / 2 - mIndicatorPaint.ascent();
            mIndicatorPaint.setColor(mIndicatorTextColor);
            canvas.drawText(formatTime, bx, bl, mIndicatorPaint);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (isLrcsEmpty()) {
            return super.onTouchEvent(event);
        }
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                removeCallbacks(mScrollRunnable);
                removeCallbacks(mHideIndicatorRunnable);
                if (!mOverScroller.isFinished()) {
                    mOverScroller.abortAnimation();
                }
                mActionDownX = event.getX();
                mActionDownY = event.getY();
                mIsActionDown = true;
                mIsActionMove = false;
                break;
            case MotionEvent.ACTION_MOVE:
                float dy = event.getY() - mActionDownY;
                if (Math.abs(dy) > mScaledTouchSlop) {
                    mIsActionMove = true;
                    mShowIndicator = true;
                }
                if (mIsActionMove) {
                    float mH = getOffsetY(mLrcs.size() - 1);
                    if (mScrollOffset < 0 || mScrollOffset > mH) {
                        dy /= 3.5f;
                    }
                    mScrollOffset -= dy;
                    mActionDownY = event.getY();
                    invalidateInner();
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if (!mIsActionMove && (!mShowIndicator || !isClickIndicatorIcon(event))) {
                    mShowIndicator = false;
                    invalidateInner();
                    performClick();
                }
                onActionUp(event);
                break;
        }
        return true;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mOverScroller.computeScrollOffset()) {
            mScrollOffset = mOverScroller.getCurrY();
            invalidateInner();
        }
    }

    public void setLrcs(List<Lrc> lrcs) {
        if (mLrcs != null) {
            mLrcs.clear();
        }
        mLrcLayoutsMap.clear();
        mLrcHeightsMap.clear();
        mCurrentLine = 0;
        mScrollOffset = 0;
        mIsActionDown = false;
        mIsActionMove = false;
        mNolrcText = TextUtils.isEmpty(mNolrcText) ? NO_LRC : mNolrcText;
        removeCallbacks(mScrollRunnable);
        invalidateInner();
        mLrcs = lrcs;
        invalidateInner();
    }

    public void resumeAutoScroll() {
        mAutoFixPosition = true;
        ViewCompat.postOnAnimationDelayed(this, mScrollRunnable, mTouchDelay);
        invalidateInner();
    }

    public void pauseAutoScroll() {
        mAutoFixPosition = false;
        invalidateInner();
    }

    public void updateCurrentLine(int time) {
        if (isLrcsEmpty()) {
            return;
        }
        int p = findUpdatePosition(time);
        if (mCurrentLine != p) {
            mCurrentLine = p;
            if (mIsActionDown) {
                invalidateInner();
                return;
            }
            ViewCompat.postOnAnimation(this, mScrollRunnable);
        }
    }

    public void setOnApplyIndicatorLineListener(OnApplyIndicatorLineListener listener) {
        mOnApplyIndicatorLineListener = listener;
    }

    private void init(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LrcView);
        mLrcTextSize = typedArray.getDimension(R.styleable.LrcView_lrcTextSize, sp2px(context, 15));
        mLrcTextLineSpan = typedArray.getDimension(R.styleable.LrcView_lrcTextLineSpan, dp2px(context, 25));
        mTouchDelay = typedArray.getInteger(R.styleable.LrcView_lrcTouchDelay, 3500);
        mIndicatorTouchDelay = typedArray.getInteger(R.styleable.LrcView_indicatorTouchDelay, 2500);
        mLrcCommonColor = typedArray.getColor(R.styleable.LrcView_lrcTextColorCommon, Color.BLACK);
        mLrcPlayingColor = typedArray.getColor(R.styleable.LrcView_lrcTextColorPlaying, Color.BLUE);
        mNolrcTextSize = typedArray.getDimension(R.styleable.LrcView_nolrcTextSize, sp2px(context, 20));
        mNolrcTextColor = typedArray.getColor(R.styleable.LrcView_nolrcTextColor, Color.BLACK);
        mIndicatorLineSpan = typedArray.getDimension(R.styleable.LrcView_indicatorLineSpan, dp2px(context, 1));
        mIndicatorTextSize = typedArray.getDimension(R.styleable.LrcView_indicatorTextSize, sp2px(context, 15));
        mIndicatorTextColor = typedArray.getColor(R.styleable.LrcView_indicatorTextColor, Color.BLACK);
        mIndicatorLrcLineColor = typedArray.getColor(R.styleable.LrcView_indicatorLrcLineColor, Color.BLACK);
        mIndicatorLineColor = typedArray.getColor(R.styleable.LrcView_indicatorLineColor, Color.BLACK);
        mIndicatorIconLeftMargin = typedArray.getDimension(R.styleable.LrcView_indicatorIconLeftMargin, dp2px(context, 0));
        mIndicatorRightMargin = typedArray.getDimension(R.styleable.LrcView_indicatorTextRightMargin, dp2px(context, 0));
        mIndicatorLineLeftToIconRightMargin = typedArray.getDimension(R.styleable.LrcView_indicatorLineLeftToIconRightMargin, dp2px(context, 0));
        mIndicatorLineRightToTextLeftMargin = typedArray.getDimension(R.styleable.LrcView_indicatorLineRightToTextLeftMargin, dp2px(context, 0));
        mIndicatorIconWidth = typedArray.getDimension(R.styleable.LrcView_indicatorIconWidth, dp2px(context, 35));
        mIndicatorIconHeight = typedArray.getDimension(R.styleable.LrcView_indicatorIconHeight, dp2px(context, 35));
        Drawable drawable = typedArray.getDrawable(R.styleable.LrcView_indicatorIcon);
        mIndicatorIcon = drawable == null ? ContextCompat.getDrawable(context, R.drawable.bg_play) : drawable;
        mLrcCurrentLineTextBold = typedArray.getBoolean(R.styleable.LrcView_lrcCurrentLineTextBold, true);
        mLndicatorLrcLineTextBold = typedArray.getBoolean(R.styleable.LrcView_indicatorLrcLineTextBold, true);
        mNolrcText = typedArray.getString(R.styleable.LrcView_nolrcText);
        typedArray.recycle();
        applyVariable(context);
    }

    private void applyVariable(Context context) {
        mBFbuilder = new BidiFormatter.Builder();
        mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mScaledMaximumFlingVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
        mScaledMinimumFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
        mOverScroller = new OverScroller(context, new DecelerateInterpolator());
        mOverScroller.setFriction(0.1f);
        mTextPaint = new TextPaint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
        mTextPaint.setTextSize(mLrcTextSize);
        mNolrcText = TextUtils.isEmpty(mNolrcText) ? NO_LRC : mNolrcText;
        mIndicatorPaint = new Paint();
        mIndicatorPaint.setAntiAlias(true);
        mIndicatorPaint.setStrokeWidth(mIndicatorLineSpan);
        mIndicatorPaint.setColor(mIndicatorLineColor);
        mIndicatorPaint.setTextSize(mIndicatorTextSize);
        mIndicatorIconRect = new Rect();
    }

    private int sp2px(Context context, float sp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, context.getResources().getDisplayMetrics());
    }

    private int dp2px(Context context, float dp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
    }

    private void onActionUp(MotionEvent event) {
        ViewCompat.postOnAnimationDelayed(this, mHideIndicatorRunnable, mIndicatorTouchDelay);
        if (mShowIndicator && mIndicatorIconRect != null && isClickIndicatorIcon(event)) {
            mShowIndicator = false;
            invalidateInner();
            if (mOnApplyIndicatorLineListener != null) {
                Lrc lrc = mLrcs.get(getIndicatorPosition());
                mOnApplyIndicatorLineListener.onApply(lrc.getTime(), lrc.getText());
            }
        }
        if (overScrolled() && mScrollOffset < 0) {
            scrollToPosition(0);
            if (mAutoFixPosition) {
                ViewCompat.postOnAnimationDelayed(this, mScrollRunnable, mTouchDelay);
            }
            return;
        }
        if (overScrolled() && mScrollOffset > getOffsetY(mLrcs.size() - 1)) {
            scrollToPosition(mLrcs.size() - 1);
            if (mAutoFixPosition) {
                ViewCompat.postOnAnimationDelayed(this, mScrollRunnable, mTouchDelay);
            }
            return;
        }
        mVelocityTracker.computeCurrentVelocity(1000, mScaledMaximumFlingVelocity);
        float yVelocity = mVelocityTracker.getYVelocity();
        float abs = Math.abs(yVelocity);
        if (abs > mScaledMinimumFlingVelocity) {
            mOverScroller.fling(0, (int) mScrollOffset, 0, (int) -yVelocity, 0, 0, 0,
                    (int) getOffsetY(mLrcs.size() - 1), 0, (int) getLineHeight(0));
            invalidateInner();
        }
        if (mVelocityTracker != null) {
            mVelocityTracker.clear();
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
        if (mAutoFixPosition) {
            ViewCompat.postOnAnimationDelayed(this, mScrollRunnable, mTouchDelay);
        }
    }

    private boolean overScrolled() {
        return mScrollOffset > getOffsetY(mLrcs.size() - 1) || mScrollOffset < 0;
    }

    private boolean isClickIndicatorIcon(MotionEvent event) {
        float left = mIndicatorIconRect.left;
        float right = mIndicatorIconRect.right;
        float top = mIndicatorIconRect.top;
        float bottom = mIndicatorIconRect.bottom;
        float x = event.getX();
        float y = event.getY();
        return mActionDownX > left && mActionDownX < right && mActionDownY > top
                && mActionDownY < bottom && x > left && x < right && y > top && y < bottom;
    }

    private void invalidateInner() {
        if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
            invalidate();
        } else {
            postInvalidate();
        }
    }

    private int findUpdatePosition(int time) {
        int p = 0;
        for (int i = 0; i < mLrcs.size(); i++) {
            Lrc lrc = mLrcs.get(i);
            if (time >= lrc.getTime()) {
                if (i == mLrcs.size() - 1) {
                    p = mLrcs.size() - 1;
                } else if (time < mLrcs.get(i + 1).getTime()) {
                    p = i;
                    break;
                }
            }
        }
        return p;
    }

    private void drawLrc(Canvas canvas, float x, float y, int p) {
        String line = mLrcs.get(p).getText();
        mBFbuilder.stereoReset(true);
        BidiFormatter bidiFormatter = mBFbuilder.build();
        String unicodeWrap = bidiFormatter.unicodeWrap(line);
        StaticLayout layout = mLrcHeightsMap.get(unicodeWrap);
        if (layout == null) {
            mTextPaint.setTextSize(mLrcTextSize);
            layout = new StaticLayout(unicodeWrap, mTextPaint, getLrcWidth(), Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false);
            mLrcHeightsMap.put(unicodeWrap, layout);
        }
        canvas.save();
        canvas.translate(x, y - layout.getHeight() / 2f - mScrollOffset);
        layout.draw(canvas);
        canvas.restore();
    }

    private int getIndicatorPosition() {
        int p = 0;
        float min = Float.MAX_VALUE;
        for (int i = 0; i < mLrcs.size(); i++) {
            float offsetY = getOffsetY(i);
            float abs = Math.abs(offsetY - mScrollOffset);
            if (abs < min) {
                min = abs;
                p = i;
            }
        }
        return p;
    }

    private void drawNolrcText(Canvas canvas) {
        mTextPaint.setTextAlign(Paint.Align.CENTER);
        mTextPaint.setColor(mNolrcTextColor);
        mTextPaint.setTextSize(mNolrcTextSize);
        canvas.save();
        StaticLayout layout = new StaticLayout(mNolrcText, mTextPaint, getLrcWidth(), Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false);
        canvas.translate(getLrcWidth() / 2f + getPaddingLeft(), getHeight() / 2f);
        layout.draw(canvas);
        canvas.restore();
    }

    private boolean isLrcsEmpty() {
        return mLrcs == null || mLrcs.isEmpty();
    }

    private void scrollToPosition(int position) {
        float scrollY = getOffsetY(position);
        ValueAnimator animator = ValueAnimator.ofFloat(mScrollOffset, scrollY);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(@NonNull ValueAnimator animation) {
                mScrollOffset = (float) animation.getAnimatedValue();
                invalidateInner();
            }
        });
        animator.setDuration(300);
        animator.start();
    }

    private float getOffsetY(int position) {
        float y = 0;
        for (int i = 1; i <= position; i++) {
            y += (getLineHeight(i - 1) + getLineHeight(i)) / 2 + mLrcTextLineSpan;
        }
        return y;
    }

    private float getLineHeight(int position) {
        String line = mLrcs.get(position).getText();
        StaticLayout layout = mLrcLayoutsMap.get(line);
        if (layout == null) {
            mTextPaint.setTextSize(mLrcTextSize);
            layout = new StaticLayout(line, mTextPaint, getLrcWidth(), Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false);
            mLrcLayoutsMap.put(line, layout);
        }
        return layout.getHeight();
    }

    private int getLrcWidth() {
        return getWidth() - getPaddingLeft() - getPaddingRight();
    }

    public void setLrcTextColorPlaying(int color){
        mLrcPlayingColor = color;
        invalidateInner();
    }
    public interface OnApplyIndicatorLineListener {
        void onApply(int time, String lrc);
    }
}
